feat(otel): add actor lifecycle + scheduler duration metrics - #514
Conversation
|
|
||
| func (s *Service) CreateActor(ctx context.Context, req *ateapipb.CreateActorRequest) (*ateapipb.Actor, error) { | ||
| if err := validateCreateActorRequest(req); err != nil { | ||
| func (s *Service) CreateActor(ctx context.Context, req *ateapipb.CreateActorRequest) (created *ateapipb.Actor, err error) { |
There was a problem hiding this comment.
why are we swapping everything to be named return args, that's not a very common pattern. If you're trying to capture err can you just do var err error. I don't feel super duper strongly about this, but I personally find it a bit harder to read.
There was a problem hiding this comment.
It's intentional, because with a var err error the defer would capture the pre-mapping error and also misqualify error.type. Would you say a comment would make this clearer?
| s.instruments.recordLifecycleOp(ctx, ateattr.OperationDelete, start, err, | ||
| ateattr.TemplateNameKey.String(tmpl.GetActorTemplateName()), | ||
| ateattr.TemplateNamespaceKey.String(tmpl.GetActorTemplateNamespace()), | ||
| ) |
There was a problem hiding this comment.
i think the behavior of this might be confusing. If this method returns before tmpl is assigned it may create a record with empty name, namespace. I think it's better to just use the incoming values even if they don't exist, what do you think?
There was a problem hiding this comment.
Fair, although DeleteActorRequest is an ObjectRef so there's no template. I'll just drop template labels from delete entirely.
There was a problem hiding this comment.
Dropped this one.
| // A nil *Instruments is a valid no-op, so call sites need no guard. Worker-count | ||
| // is registered separately (RegisterWorkerCount): a callback-driven observable, | ||
| // not a synchronous instrument. | ||
| type Instruments struct { |
There was a problem hiding this comment.
Is there any reason this is public, seems like we could construct it locally rather than exporting it
There was a problem hiding this comment.
It's exported because main.go builds it and passes it to NewService like e.g. RegisterWorkerCount, so this seemed like the consistent way.
72daaf6 to
00fb7ca
Compare
26c376a to
cd0945a
Compare
|
cc. Da Huang (@git286), Julian Gutierrez Oschmann (@juli4n) for review. This has been up for a good while now, can you please review when you have a chance? |
cd0945a to
1c136ae
Compare
| // Recorded before the lock so lock contention still counts as an attempt; the | ||
| // incoming ctx stays valid where acquireActorLock returns nil on failure. | ||
| defer func() { | ||
| w.instruments.recordLifecycleOp(ctx, ateattr.OperationResume, start, err, | ||
| lifecycleOpAttrs(state.Actor, state.ActorTemplate, state.SnapshotKind)...) | ||
| }() | ||
|
|
||
| lockCtx, lock, err := w.acquireActorLock(ctx, actorRef) |
There was a problem hiding this comment.
ResumeActor is called on every event, and ActorResumer has no local "already running" cache — it always issues the ResumeActor RPC. That's exactly why ResumeState.WasRunning and the Resumed response field exist (ResumeOutcomeNone = "actor already running" in the router's own docs table).
This defer records unconditionally, so the sample rate of ate.actor.lifecycle.operation.duration{operation="resume"} equals router QPS, and the population is dominated by sub-millisecond no-op resumes that fast-forward every step. The p50/p99 you'd read off this is the warm-path no-op latency, not "when a suspended actor can serve requests again" — which is not the stated goal of the metric.
Consider adding a check here:
if err == nil && state.WasRunning {
return // already running: not a resume, and the router calls this per request
}
There was a problem hiding this comment.
Added your guard in the latest push + a test pinning it.
| actorTemplateLister listersv1alpha1.ActorTemplateLister | ||
| workerPoolLister listersv1alpha1.WorkerPoolLister | ||
| actorWorkflow *ActorWorkflow | ||
| instruments *Instruments |
There was a problem hiding this comment.
The instrument is threaded into two different layers:
- create / delete are recorded in the gRPC handler (create_actor.go, delete_actor.go), so they include request validation, lister lookups, and the final gRPC status.
- resume / suspend / pause are recorded inside
ActorWorkflow(workflow.go), so they exclude the handler's validation.
Can we handle all metrics in gRPC handlers to align?
There was a problem hiding this comment.
The split is intentional to some extent. The workflow owns the labels, so recording in handlers would drop those dims from exactly the failure datapoints they're for. But I agree about this not being fully consistent as validation failures currently count create/delete and not the workflow operations.
I'd rather fix that by moving the create/delete recording after validation, so all five operations measure a validated operation, and keep malformed requests visible in rpc.server.call.duration. Wdyt?
Tim Bai (baizhenyu)
left a comment
There was a problem hiding this comment.
Thanks for this. The WasRunning skip in the latest commit is a good fix, and reusing the existing signal means the metric and the resumed return value cannot disagree.
I have left some questions inline about the metric definitions. I would like to settle these before we build dashboards on top of them.
…ider buckets, delete template dims
This PR is the second slice of the platform-metrics split (#433).
It adds two duration histograms emitted by ateapi:
ate.actor.lifecycle.operation.duration: create/resume/suspend/pause/delete, labeled by operation, template, pool, sandbox class, and (on resume) snapshot kindrpc.server.call.durationmetric covers this, but the meaningful dimensions are missing, so it's not really actionable. Extending its labels with extra, domain-specific labels is an OTel anti-pattern, hence the new metric.ate.scheduler.assignment.duration: worker-assignment step, labeled by outcome (assigned / no_free_worker / error) and poolTested e2e on a local kind cluste where: both metrics reach the otel-system collector with the expected labels (resume shows
snapshot_kind=golden, scheduler showsoutcome=assigned, noerror.typeon success).